package org.vaadin.nf4v.core.config;

import com.vaadin.ui.Component;
import org.vaadin.nf4v.core.NavigableWindow;
import org.vaadin.nf4v.core.NavigationException;
import org.vaadin.nf4v.core.handler.Handler;
import org.vaadin.nf4v.core.navigation.NavigationManager;
import org.vaadin.nf4v.core.navigation.NavigationRequest;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Navigation configuration.
 * Represents configuration of entire application.
 * <p/>
 * Navigation configuration is accessible through NavigableApplication#getNavigationConfiguration().
 * It is intended to be build with builder (like NavigationConfigBuilder).
 * <p/>
 * Contains:
 * 1. all contexts configurations
 * 2. all instances of handlers (including overridden handlers)
 */
public class NavigationConfig extends AbstractConfig {

    private Map<String, ContextConfig> contexts = new HashMap<String, ContextConfig>();
    // handler instances: handler implementation class -> handler instance (all handlers are stateless!)
    private Map<Class<? extends Handler>, Handler> handlerInstanceMap = new HashMap<Class<? extends Handler>, Handler>();

    // todo default flag for auto cross context links (think)

    /**
     * Adds context
     *
     * @param contextConfig context config
     * @throws ContextConfigurationException if context is already registered
     */
    public void addContextConfig(ContextConfig contextConfig) throws ContextConfigurationException {
        String context = contextConfig.getName();
        if (isContextExists(context)) {
            throw new ContextConfigurationException("Context '" + context + "' already registered");
        }
        contexts.put(context, contextConfig);
    }

    /**
     * Obtains handler implementation instance (not creates it!
     * all handler instances are created during configuration phase).
     *
     * @param handlerInstanceClass handler implementation class (not handler interface!)
     * @return handler instance or null if handler with provided class is not registered
     */
    @SuppressWarnings("unchecked")
    public <T extends Handler> T getHandlerInstance(Class<T> handlerInstanceClass) {
        return (T) handlerInstanceMap.get(handlerInstanceClass);
    }

    /**
     * Shortcut method.
     *
     * @param handlerClass handler interface class
     * @param request      navigation request
     * @return handler instance or null if not found
     * @see NavigationConfig#getHandler(Class, String, Class)
     */
    public <T extends Handler> T getHandler(Class<T> handlerClass, NavigationRequest request) {
        return getHandler(handlerClass, request.getTarget().getContext() == null ?
                NavigationManager.getNavigationContext() : request.getTarget().getContext(),
                request.getTarget().getPageClass());
    }

    /**
     * Shortcut method.
     *
     * @param handlerClass handler interface class
     * @return handler instance or null if not found
     * @see NavigationConfig#getHandler(Class, String, Class)
     */
    public <T extends Handler> T getHandler(Class<T> handlerClass) {
        return getHandler(handlerClass, null, null);
    }

    /**
     * Shortcut method.
     *
     * @param handlerClass handler interface class
     * @param context      context
     * @return handler instance or null if not found
     * @see NavigationConfig#getHandler(Class, String, Class)
     */
    public <T extends Handler> T getHandler(Class<T> handlerClass, String context) {
        return getHandler(handlerClass, context, null);
    }

    /**
     * Obtains handler instance.
     * Tries to find default handler overriding first:
     * 1. if page provided in page config
     * 2. if context provided in context config
     * 3. if no overriding are found returns default handler
     *
     * @param handlerClass handler interface class
     * @param context      context
     * @param pageClass    page class
     * @return handler instance (overridden or default) or null if no implementation found
     *         (but this should be impossible, because configuration validation)
     */
    @SuppressWarnings("unchecked")
    public <T extends Handler> T getHandler(Class<T> handlerClass, String context, Class<? extends Component> pageClass) {
        T handler;
        if (context != null) {
            if (pageClass != null) {
                handler = resolveHandler(getPageConfig(context, pageClass), handlerClass);
                if (handler != null) {
                    return handler;
                }
            }
            handler = resolveHandler(getContextConfig(context), handlerClass);
            if (handler != null) {
                return handler;
            }
        }
        return resolveHandler(this, handlerClass);
    }

    /**
     * Searches for handler in configuration attributes:
     * If page or context configuration wants to override default handler, it should register
     * attribute with key= handler interface full name and value = implementation class.
     *
     * @param config       configuration
     * @param handlerClass handler interface class
     * @return handler instance ot null if not found n configuration
     * @see org.vaadin.nf4v.core.config.process.NavigationConfigProcessor#registerHandler(AbstractConfig, Class)
     * @see org.vaadin.nf4v.core.config.process.NavigationConfigProcessor#registerHandler(AbstractConfig, org.vaadin.nf4v.core.handler.Handler)
     */
    @SuppressWarnings("unchecked")
    protected <T extends Handler> T resolveHandler(AbstractConfig config, Class<T> handlerClass) {
        Class<Handler> implClazz = config.getAttribute(handlerClass.getName());
        if (implClazz != null) {
            T handler = (T) handlerInstanceMap.get(implClazz);
            if (handler == null) {
                throw new ContextConfigurationException("Wrong context configuration: instance is not registered for handler " +
                        implClazz.getName());
            }
            return handler;
        }
        return null;
    }

    /**
     * Sets handler implementation instance.
     *
     * @param handlerClass handler implementation class
     * @param handler      handler instance
     * @throws ContextConfigurationException if instance is null or handler is already registered
     */
    public <T extends Handler> void setHandlerInstance(Class<T> handlerClass, T handler) throws ContextConfigurationException {
        // todo check class and impl compatibility
        if (handler == null) {
            throw new ContextConfigurationException("Handler instance can't be null");
        }
        // todo FIXME limitation: it is not possible to register same handler implementations but differently configured
        if (getHandlerInstance(handlerClass) != null) {
            throw new ContextConfigurationException("Handler instance with class " + handler.getClass() + " is already registered");
        }
        handlerInstanceMap.put(handlerClass, handler);
    }

    public boolean isContextExists(String context) {
        return contexts.containsKey(context);
    }

    /**
     * Recognizes context name in window name.
     * Window name may be different from context name, cause Vaadin modifies it to differentiate
     * windows in different browser tabs.
     *
     * @param windowName window name
     * @return context name if it was recognized in provided window name, null otherwise
     */
    public String resolveNavigationWindowContext(String windowName) {
        String name = windowName.split("_")[0]; // in case of second tab window name will be context_tabNumber
        for (String context : contexts.keySet()) {
            if (context.equals(name)) return context;
        }
        return null;
    }

    /**
     * @param context context name
     * @return context configuration for context
     * @throws NavigationException if context not recognized
     */
    public ContextConfig getContextConfig(String context) throws NavigationException {
        ContextConfig config = contexts.get(context);
        if (config == null) {
            throw new NavigationException("Context '" + context + "' is not registered");
        }
        return config;
    }

    // todo remove?

    /**
     * @param context context name
     * @return window class for context
     * @throws NavigationException if context not recognized
     */
    public Class<? extends NavigableWindow> getWindowClass(String context) throws NavigationException {
        ContextConfig config = contexts.get(context);
        if (config == null) {
            throw new NavigationException("Context '" + context + "' is not registered");
        }
        return config.getWindowClass();
    }

    // todo remove?

    /**
     * @param context context name
     * @param uri     page uri
     * @return page class or null if page not found for uri
     * @throws NavigationException if context not recognized
     */
    public Class<? extends Component> getPageClass(String context, String uri) throws NavigationException {
        // todo uri null check
        ContextConfig config = contexts.get(context);
        if (config == null) {
            throw new NavigationException("Context '" + context + "' is not registered.");
        }
        PageConfig pageConfig = config.getPageByUri(uri);
        return pageConfig == null ? null : pageConfig.getPageClass();
    }

    /**
     * @param context context name
     * @return home page class for context
     * @throws NavigationException if context not recognized
     */
    public Class<? extends Component> getHomePageClass(String context) throws NavigationException {
        ContextConfig config = contexts.get(context);
        if (config == null) {
            throw new NavigationException("Context '" + context + "' is not registered.");
        }
        // home page always exist, cause configuration will not start without correct config
        return config.getHomePage().getPageClass();
    }

    /**
     * @return main context name
     */
    public String getMainContext() {
        // todo index
        for (ContextConfig config : contexts.values()) {
            if (config.isMainWindow()) return config.getName();
        }
        // unreachable point cause application will not start with wrong configuration
        return null;
    }

    /**
     * @param context   context
     * @param pageClass page class
     * @return page config or null if page not found in context
     * @throws NavigationException if context not recognized
     */
    public PageConfig getPageConfig(String context, Class<? extends Component> pageClass) throws NavigationException {
        ContextConfig config = contexts.get(context);
        if (config == null) {
            throw new NavigationException("Context '" + context + "' is not registered.");
        }
        PageConfig pageConfig = config.getPageByClass(pageClass);
        if (pageConfig != null) {
            return pageConfig;
        }
        return null;
    }

    /**
     * @return list of all contexts
     */
    public List<ContextConfig> getContextConfigs() {
        return new ArrayList<ContextConfig>(contexts.values());
    }

    //todo get contextconfig and page config form navigation target
}
